route.test.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /* @vitest-environment node */
  2. import { describe, it, expect, vi, beforeEach } from "vitest";
  3. vi.mock("@/lib/auth/session", () => ({
  4. getSession: vi.fn(),
  5. }));
  6. vi.mock("@/lib/db", () => ({
  7. getDb: vi.fn(),
  8. }));
  9. vi.mock("@/models/user", () => {
  10. const USER_ROLES = Object.freeze({
  11. BRANCH: "branch",
  12. ADMIN: "admin",
  13. SUPERADMIN: "superadmin",
  14. DEV: "dev",
  15. });
  16. return {
  17. default: {
  18. findById: vi.fn(),
  19. findOne: vi.fn(),
  20. },
  21. USER_ROLES,
  22. };
  23. });
  24. import { getSession } from "@/lib/auth/session";
  25. import { getDb } from "@/lib/db";
  26. import User from "@/models/user";
  27. import { PATCH, dynamic } from "./route.js";
  28. function createRequestStub(body) {
  29. return {
  30. async json() {
  31. return body;
  32. },
  33. };
  34. }
  35. describe("PATCH /api/admin/users/[userId]", () => {
  36. beforeEach(() => {
  37. vi.clearAllMocks();
  38. getDb.mockResolvedValue({});
  39. });
  40. it('exports dynamic="force-dynamic"', () => {
  41. expect(dynamic).toBe("force-dynamic");
  42. });
  43. it("returns 401 when unauthenticated", async () => {
  44. getSession.mockResolvedValue(null);
  45. const res = await PATCH(createRequestStub({}), {
  46. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  47. });
  48. expect(res.status).toBe(401);
  49. expect(await res.json()).toEqual({
  50. error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
  51. });
  52. });
  53. it("returns 403 when authenticated but not allowed (admin)", async () => {
  54. getSession.mockResolvedValue({
  55. userId: "u1",
  56. role: "admin",
  57. branchId: null,
  58. email: "admin@example.com",
  59. });
  60. const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
  61. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  62. });
  63. expect(res.status).toBe(403);
  64. expect(await res.json()).toEqual({
  65. error: {
  66. message: "Forbidden",
  67. code: "AUTH_FORBIDDEN_USER_MANAGEMENT",
  68. },
  69. });
  70. });
  71. it("returns 400 when JSON parsing fails", async () => {
  72. getSession.mockResolvedValue({
  73. userId: "u2",
  74. role: "superadmin",
  75. branchId: null,
  76. email: "superadmin@example.com",
  77. });
  78. const req = { json: vi.fn().mockRejectedValue(new Error("invalid json")) };
  79. const res = await PATCH(req, {
  80. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  81. });
  82. expect(res.status).toBe(400);
  83. expect(await res.json()).toEqual({
  84. error: {
  85. message: "Invalid request body",
  86. code: "VALIDATION_INVALID_JSON",
  87. },
  88. });
  89. });
  90. it("returns 400 when body is not an object", async () => {
  91. getSession.mockResolvedValue({
  92. userId: "u2",
  93. role: "superadmin",
  94. branchId: null,
  95. email: "superadmin@example.com",
  96. });
  97. const res = await PATCH(createRequestStub("nope"), {
  98. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  99. });
  100. expect(res.status).toBe(400);
  101. expect(await res.json()).toEqual({
  102. error: {
  103. message: "Invalid request body",
  104. code: "VALIDATION_INVALID_BODY",
  105. },
  106. });
  107. });
  108. it("returns 400 when userId param is missing", async () => {
  109. getSession.mockResolvedValue({
  110. userId: "u2",
  111. role: "dev",
  112. branchId: null,
  113. email: "dev@example.com",
  114. });
  115. const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
  116. params: Promise.resolve({ userId: undefined }),
  117. });
  118. expect(res.status).toBe(400);
  119. expect(await res.json()).toEqual({
  120. error: {
  121. message: "Missing required route parameter(s)",
  122. code: "VALIDATION_MISSING_PARAM",
  123. details: { params: ["userId"] },
  124. },
  125. });
  126. });
  127. it("returns 400 when userId param is invalid", async () => {
  128. getSession.mockResolvedValue({
  129. userId: "u2",
  130. role: "dev",
  131. branchId: null,
  132. email: "dev@example.com",
  133. });
  134. const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
  135. params: Promise.resolve({ userId: "nope" }),
  136. });
  137. expect(res.status).toBe(400);
  138. expect(await res.json()).toMatchObject({
  139. error: { code: "VALIDATION_INVALID_FIELD" },
  140. });
  141. });
  142. it("returns 404 when user does not exist", async () => {
  143. getSession.mockResolvedValue({
  144. userId: "u2",
  145. role: "superadmin",
  146. branchId: null,
  147. email: "superadmin@example.com",
  148. });
  149. User.findById.mockReturnValue({
  150. exec: vi.fn().mockResolvedValue(null),
  151. });
  152. const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
  153. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  154. });
  155. expect(res.status).toBe(404);
  156. expect(await res.json()).toEqual({
  157. error: {
  158. message: "Not found",
  159. code: "USER_NOT_FOUND",
  160. details: { userId: "507f1f77bcf86cd799439011" },
  161. },
  162. });
  163. });
  164. it("returns 400 when switching to role=branch without branchId (existing has none)", async () => {
  165. getSession.mockResolvedValue({
  166. userId: "u2",
  167. role: "dev",
  168. branchId: null,
  169. email: "dev@example.com",
  170. });
  171. const user = {
  172. _id: "507f1f77bcf86cd799439011",
  173. username: "x",
  174. email: "x@example.com",
  175. role: "admin",
  176. branchId: null,
  177. mustChangePassword: false,
  178. createdAt: new Date(),
  179. updatedAt: new Date(),
  180. save: vi.fn(),
  181. };
  182. User.findById.mockReturnValue({
  183. exec: vi.fn().mockResolvedValue(user),
  184. });
  185. const res = await PATCH(createRequestStub({ role: "branch" }), {
  186. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  187. });
  188. expect(res.status).toBe(400);
  189. expect(await res.json()).toEqual({
  190. error: {
  191. message: "Missing required fields",
  192. code: "VALIDATION_MISSING_FIELD",
  193. details: { fields: ["branchId"] },
  194. },
  195. });
  196. });
  197. it("returns 200 and updates fields; clears branchId for non-branch roles", async () => {
  198. getSession.mockResolvedValue({
  199. userId: "u2",
  200. role: "superadmin",
  201. branchId: null,
  202. email: "superadmin@example.com",
  203. });
  204. const user = {
  205. _id: "507f1f77bcf86cd799439011",
  206. username: "olduser",
  207. email: "old@example.com",
  208. role: "branch",
  209. branchId: "NL01",
  210. mustChangePassword: true,
  211. createdAt: new Date("2026-02-01T10:00:00.000Z"),
  212. updatedAt: new Date("2026-02-02T10:00:00.000Z"),
  213. save: vi.fn().mockResolvedValue(true),
  214. };
  215. User.findById.mockReturnValue({
  216. exec: vi.fn().mockResolvedValue(user),
  217. });
  218. // No uniqueness checks needed here (we only change role + mustChangePassword)
  219. const res = await PATCH(
  220. createRequestStub({
  221. role: "admin",
  222. mustChangePassword: false,
  223. }),
  224. {
  225. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  226. },
  227. );
  228. expect(res.status).toBe(200);
  229. // Role changed => branchId must be cleared
  230. expect(user.role).toBe("admin");
  231. expect(user.branchId).toBe(null);
  232. expect(user.mustChangePassword).toBe(false);
  233. expect(user.save).toHaveBeenCalledTimes(1);
  234. const body = await res.json();
  235. expect(body).toMatchObject({
  236. ok: true,
  237. user: {
  238. id: "507f1f77bcf86cd799439011",
  239. username: "olduser",
  240. email: "old@example.com",
  241. role: "admin",
  242. branchId: null,
  243. mustChangePassword: false,
  244. },
  245. });
  246. });
  247. it("returns 400 when username is already taken by another user", async () => {
  248. getSession.mockResolvedValue({
  249. userId: "u2",
  250. role: "dev",
  251. branchId: null,
  252. email: "dev@example.com",
  253. });
  254. const user = {
  255. _id: "507f1f77bcf86cd799439011",
  256. username: "olduser",
  257. email: "old@example.com",
  258. role: "admin",
  259. branchId: null,
  260. mustChangePassword: false,
  261. createdAt: new Date(),
  262. updatedAt: new Date(),
  263. save: vi.fn(),
  264. };
  265. User.findById.mockReturnValue({
  266. exec: vi.fn().mockResolvedValue(user),
  267. });
  268. User.findOne.mockReturnValue({
  269. select: vi.fn().mockReturnThis(),
  270. exec: vi.fn().mockResolvedValue({ _id: "507f1f77bcf86cd799439099" }),
  271. });
  272. const res = await PATCH(createRequestStub({ username: "TakenUser" }), {
  273. params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
  274. });
  275. expect(res.status).toBe(400);
  276. const body = await res.json();
  277. expect(body).toEqual({
  278. error: {
  279. message: "Username already exists",
  280. code: "VALIDATION_INVALID_FIELD",
  281. details: { field: "username", value: "takenuser" },
  282. },
  283. });
  284. });
  285. });